package org.stars.daostars;


import java.io.Serializable;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.LinkedList;
import java.util.List;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.stars.dao.exception.DaoException;
import org.stars.daostars.core.DaoStars;
import org.stars.database.ConnectionPoolManager;
import org.stars.util.StringUtility;


/**
 * <p>
 * E' la classe di utilità che permette di aprire una transazione, effettuare le
 * operazioni necessarie ed effettuare a seconda dell'esito un commit o una
 * rollback.</b>
 * </p>
 * <p>
 * Esempio di caso utilizzando un oggetto di DaoService per gestione
 * transazione:
 * </p>
 * <code>
 *   <br>
 *    DaoPersone daoPersone = new DaoPersone();<br>
 *    DaoAziende daoAziende = new DaoAziende();<br>
 *    <br>
 *    DaoService session = new DaoSession();<br>
 *    session = beginTransaction)();<br>
 *    <br>
 *    session.addDao(daoPersone);<br>
 *    session.addDao(daoAziende);<br>
 *    <br>
 *    try{<br>
 *    <br>
 * 	  daoPersone.recupera(..);<br>
 * 	  daoAzienda.recupera(..);<br>
 *    daoPersone.aggiorna(..);<br>
 *    daoAziende.aggiorna(..);<br>
 *    <br>
 *    session.commitTransaction();<br>
 *    } catch(Exception e){<br>
 *      session.rollbackTransaction();<br>
 *    }<br>
 * </code>
 * 
 * @author Francesco Benincasa (908099)
 * @version 1.0.0, 13/07/2006
 */
public class DaoSession implements Serializable {

	/**
	 * Restituisce il tipo di sessione. Ricordiamo che le sessioni possono
	 * essere di due tipi:
	 * <ul>
	 * <li>shared mode: connessione condivisa tra più dao</li>
	 * <li>transaction mode: transazione jdbc condivisa tra più dao</li>
	 * </ul>
	 * Questo metodo restituisce una stringa a seconda del tipo di connessione
	 * utilizzata.
	 * 
	 * @return tipo di sessione sotto forma di stringa
	 */
	String getStringMode() {
		if (isSharedConnectionOpened()) {
			return "shared mode";
		}
		return "transaction mode";
	}

	/**
	 * nome da mettere nel caso in cui la connessione non esiste.
	 */
	protected static final String NO_POOLED_CONNECTION_NAME = "<NoPooledConnectionName>";

	/**
	 * serial id
	 */
	private static final long serialVersionUID = 67464952586582559L;

	/**
	 * Abilita il test mode per le transazioni
	 */
	private static boolean testMode;

	/**
	 * Logger
	 * 
	 * @return logger
	 */
	protected Log getLog() {
		return LogFactory.getLog(DaoSession.class);
	}

	/**
	 * indica se la stiamo utilizzando il daoService in modalit� transazionale o
	 * se lo stiamo utilizzando semplicemente per far condividere a pi� dao la
	 * stessa connessione.
	 */
	private boolean transactionMode;

	/**
	 * nome della connessione
	 */
	private String connectionName;

	/**
	 * <p>
	 * Restituisce <code>true</code> nel caso l'oggetto si trova in modalit�
	 * <i>test</i>, <code>false</code> se sta operando in modalit� normale.
	 * 
	 * @return Returns the testMode.
	 * 
	 */
	public static boolean isTestMode() {
		return testMode;
	}

	/**
	 * Imposta la modalit� test. Se impostata a <code>true</code>, tutte le
	 * operazioni di <b>insert, update e delete</b> non verranno rese
	 * persistenti su db. Per tornare alla modalità normale è sufficiente
	 * impostare come parametro di questo metodo <code>false</code>.
	 * 
	 * @param value
	 *            modalità da impostare
	 * 
	 */
	public static void setTestMode(boolean value) {
		testMode = value;
	}

	/**
	 * Connessione utilizzata per la transazione
	 */
	private transient Connection conn;

	/**
	 * Elenco di oggetti Dao coinvolti nella transazione.
	 */
	private List<DaoSessionWorker> dao;

	/**
	 * Costruttore. Inizializza la lista di oggetti
	 */
	public DaoSession() {
		setUp(DaoStars.getInstance().getDefaultConnectionName());
	}

	/**
	 * Inizializza l'array di dao ed imposta ai valori di default le
	 * connessioni.
	 * 
	 * @param sConnectionName
	 *            nome del connection pool dal quale recuperare la connessione
	 */
	protected void setUp(String sConnectionName) {
		dao = new LinkedList<DaoSessionWorker>();
		transactionMode = true;
		conn = null;
		connectionName = StringUtility.nvl(sConnectionName).toUpperCase();
	}

	/**
	 * Costruttore che richiede il nome del connection pool da utilizzare per la
	 * gestione della transazione.
	 * 
	 * @param connectionName
	 *            nome del connection pool
	 */
	public DaoSession(String connectionName) {
		setUp(connectionName);
	}

	/**
	 * Inizia la transazione. I Dao devono essere aggiunti DOPO l'inizio della
	 * transazione. I dao presenti prima vengono rimossi.
	 * 
	 * @throws SQLException
	 *             in caso di errore.
	 */
	public void beginTransation() throws SQLException {
		beginTransation(null);
	}

	/**
	 * Inizia la transazione. I Dao devono essere aggiunti DOPO l'inizio della
	 * transazione. I dao presenti prima vengono rimossi.
	 * 
	 * @param standAloneConnection
	 *            connessione da utilizzare nella sessione.
	 * 
	 * @throws SQLException
	 *             in caso di errore.
	 */
	public void beginTransation(Connection standAloneConnection) throws SQLException {
		Log log = getLog();
		if (conn != null) {
			String sMessage = "BEGIN TRANSACTION - ERRORE: connessione gia' aperta";
			if (DaoBase.isGlobalPrintMode()) {
				log.error(sMessage);
			}
			try {
				conn.close();
			} catch (Exception e) { /* do nothing */
			}
			SQLException ex = new SQLException(sMessage);
			log.error(ex);
			throw ex;
		}

		if (standAloneConnection != null) {
			connectionName = NO_POOLED_CONNECTION_NAME;
			conn = standAloneConnection;
		} else {
			conn = getConnection();
		}

		conn.setAutoCommit(false);
		dao.clear();
		transactionMode = true;

		if (DaoBase.isGlobalPrintMode()) {
			log.debug("BEGIN TRANSACTION " + connectionName);
		}
	}

	/**
	 * Apre una connessione. Questa metodo consente di condividere la stessa
	 * connesssione tra più dao. I Dao devono essere aggiunti DOPO l'apertura
	 * della connessione. I dao presenti prima vengono rimossi.
	 * 
	 * @throws SQLException
	 *             in caso di errore.
	 * @throws DaoException
	 *             in caso di errore.
	 */
	public void openConnection() throws SQLException, DaoException {
		openConnection(null);
	}

	/**
	 * Apre una connessione. Questa metodo consente di condividere la stessa
	 * connesssione tra più dao. I Dao devono essere aggiunti DOPO l'apertura
	 * della connessione. I dao presenti prima vengono rimossi.
	 * 
	 * @param standAloneConnection
	 *            connessione da utilizzare nella sessione
	 * @throws SQLException
	 *             in caso di errore.
	 * @throws DaoException
	 *             in caso di errore.
	 */
	public void openConnection(Connection standAloneConnection) throws SQLException, DaoException {
		Log log = getLog();
		if (conn != null) {
			String sMessage = "OPEN SHARED CONNECTION - ERRORE: connessione gia' aperta";
			if (DaoBase.isGlobalPrintMode()) {
				log.error(sMessage);
			}
			try {
				conn.close();
			} catch (Exception e) { /* do nothing */
			}
			DaoException ex = new DaoException(sMessage);
			log.error(ex);
			throw ex;
		}

		if (standAloneConnection != null) {
			connectionName = NO_POOLED_CONNECTION_NAME;
			conn = standAloneConnection;
		} else {
			conn = getConnection();
		}

		if (DaoBase.isTestMode())
			conn.setAutoCommit(false);
		else
			conn.setAutoCommit(true);

		dao.clear();
		transactionMode = false;

		if (DaoBase.isGlobalPrintMode()) {
			log.debug("OPEN SHARED CONNECTION " + connectionName);
		}
	}

	/**
	 * Restituisce <code>true</code> se è stata aperta una connessione o una
	 * transazione.
	 * 
	 * @return <code>true</code> se la connessione o transazione è aperta.
	 */
	public boolean isOpened() {
		if (conn != null)
			return true;
		return false;
	}

	/**
	 * Restituisce <code>true</code> se è stata aperta una transazione. Nel caso
	 * di connessione aperta viene restiuto <code>false</code>.
	 * 
	 * @return <code>true</code> se la transazione è aperta.
	 */
	public boolean isTransactionOpened() {
		if (conn != null && transactionMode)
			return true;
		return false;
	}

	/**
	 * Restituisce <code>true</code> se è stata aperta una connession. Nel caso
	 * di transazione aperta viene restiuto <code>false</code>.
	 * 
	 * @return <code>true</code> se la connessione è aperta.
	 */
	public boolean isSharedConnectionOpened() {
		if (conn != null && !transactionMode)
			return true;
		return false;
	}

	/**
	 * Restituisce il nome del connection pool che contiene la connessione.
	 * 
	 * @return nome del connection pool utilizzato
	 */
	public String getConnectionName() {
		return connectionName;
	}

	/**
	 * Chiude la connessione. La connessione deve essere aperta mediante il il
	 * metodo <code>openConnection</code>.
	 * 
	 * @throws SQLException
	 * @throws DaoException
	 */
	public void closeConnection() throws SQLException, DaoException {
		Log log = getLog();
		// prima di fare altro cancelliamo la connessione
		Connection objConn = this.conn;
		conn = null;

		deleteTransaction();

		if (transactionMode) {
			String sMsg = "TENTATIVO DI CHIUDERE UNA TRANSAZIONE SENZA COMMIT";
			if (DaoBase.isGlobalPrintMode()) {
				log.debug(sMsg);
			}
			rollbackTransaction(); // rollback
			DaoException ex = new DaoException(sMsg);
			log.error(ex);
			throw ex;
		}

		// se siamo in testmode facciamo comunque un rollback
		if (isTestMode()) {
			objConn.rollback();
			if (DaoBase.isGlobalPrintMode()) {
				log.debug("CLOSE SHARED CONNECTION " + connectionName + " (TEST MODE)");
			}

		} else {
			if (DaoBase.isGlobalPrintMode()) {
				log.debug("CLOSE SHARED CONNECTION" + connectionName + " ");
			}

		}
		objConn.setAutoCommit(true);
		objConn.close();

		objConn = null;

		transactionMode = true;
	}

	/**
	 * Aggiunge un dao ad una connessione o ad una transazione. Questo metodo
	 * deve essere invocato solo dopo aver aperto la connessione. Nel caso venga
	 * aggiunto un dao prima del'apertura della connessione, verr� sollevata
	 * un'eccezione.
	 * 
	 * @param value
	 *            dao da aggiungere alla transazione/connessione.
	 * @return boolean <code>true</code> se tutto va bene.
	 * 
	 * @throws java.sql.SQLException
	 * @throws DaoException
	 */
	public boolean addDao(DaoSessionWorker value) throws SQLException, DaoException {
		Log log = getLog();
		if (conn == null) {
			// stiamo tentando di aggiungere un dao ad una transazione che
			// non è stata ancora avviata.
			String sMsg = "Impossibile aggiungere un dao (" + value.toString() + ") ad una transazione non ancora aperta."
					+ " Forse non hai messo prima del metodo addDao il metodo beginTransaction o del metodo openConnection?";

			if (DaoBase.isGlobalPrintMode()) {
				log.debug(sMsg);
			}

			DaoException ex = new DaoException(sMsg);
			log.error(ex);
			throw ex;

		}
		if (value == null)
			return false;

		// il dao e' gia' e' legato ad una Sessione, che non e' questa
		if (value.getDaoSession() != null && !this.equals(value.getDaoSession())) {
			String sMsg = "";

			// Errore: il connection pool della sessione già utilizzata è
			// diversa dal nome di questa sessione (a prescindere
			// dal tipo di sessione )
			if (!value.getDaoSession().getConnectionName().equalsIgnoreCase(this.getConnectionName())) {
				sMsg = "Impossibile aggiungere un dao (" + value.getClass().toString() + ") che usa un connection pool (" + value.getDaoSession().getConnectionName()
						+ ") ad una session che utilizza un altro " + " connection pool (" + connectionName + ")";

				DaoException ex = new DaoException(sMsg);
				log.error(ex);
				throw ex;
			}

			// Errore: le due sessioni sono collegate allo stesso connection
			// pool ma in modalità diverse
			if (value.getDaoSession().getConnectionName().equalsIgnoreCase(getConnectionName()) && value.getDaoSession().isSharedConnectionOpened() != isSharedConnectionOpened()) {
				sMsg = "Impossibile aggiungere il dao (" + value.getClass().toString() + ") che lavora in " + value.getDaoSession().getStringMode() + "  con il connection pool ("
						+ value.getDaoSession().getConnectionName() + ") ad una sessione che utilizza il " + " connection pool (" + connectionName + ") ma in modalità "
						+ getStringMode();

				DaoException ex = new DaoException(sMsg);
				log.error(ex);
				throw ex;
			}

		}

		// lo aggiungo alla sessione solo nel caso in cui
		// non sia stato gia' fatto
		if (!this.equals(value.getDaoSession())) {
			value.setTransaction(conn);
			value.setDaoSession(this);

			this.dao.add(value);
		}

		if (DaoBase.isGlobalPrintMode()) {
			log.debug("ADD DAO : " + value.getClass().toString());
		}

		return true;
	}

	/**
	 * Effettua il commit di una transazione, e nel caso il parametro
	 * <code>closeConnection</code> sia impostato a <code>false</code> lascia la
	 * connessione aperta e tutti i dao collegati, altrimenti chiude tutto.
	 * 
	 * @param closeConnection
	 *            se <code>true</code> indica di chiudere la connessione e di
	 *            scollegare i dao collegati.
	 * @throws SQLException 			
	 * @throws DaoException
	 */
	public void commitTransaction(boolean closeConnection) throws SQLException, DaoException {
		Log log = getLog();
		// prima di fare altro cancelliamo la connessione
		Connection objConn = this.conn;

		if (closeConnection) {
			deleteTransaction();
			conn=null;
		}

		if (!transactionMode) {
			String sMsg = "TENTATIVO DI CHIUDERE UNA TRANSAZIONE COME CONNESSIONE NORMALE";
			if (DaoBase.isGlobalPrintMode()) {
				log.debug(sMsg);
			}
			rollbackTransaction(closeConnection); // rollback

			DaoException ex = new DaoException(sMsg);
			log.error(ex);
			throw ex;
		}

		// se siamo in testmode facciamo comunque un rollback
		if (isTestMode()) {
			objConn.rollback();
			if (DaoBase.isGlobalPrintMode()) {
				log.debug("CLOSE TRANSACTION " + connectionName + " COMMIT (TEST MODE)");
			}

		} else {
			objConn.commit();
			if (DaoBase.isGlobalPrintMode()) {
				log.debug("CLOSE TRANSACTION " + connectionName + " COMMIT");
			}

		}

		if (closeConnection) {
			objConn.setAutoCommit(true);
			objConn.close();
		}
	}

	/**
	 * Effettua il commit di una transazione, e nel caso il parametro
	 * <code>closeConnection</code> sia impostato a <code>false</code> lascia la
	 * connessione aperta e tutti i dao collegati, altrimenti chiude tutto.
	 * 
	 * @param closeConnection
	 *            se <code>true</code> indica di chiudere la connessione e di
	 *            scollegare i dao collegati.
	 * @throws SQLException
	 * @throws DaoException
	 */
	public void rollbackTransaction(boolean closeConnection) throws SQLException, DaoException {
		Log log = getLog();
		// prima di fare altro cancelliamo la connessione
		Connection objConn = this.conn;
		
		if (closeConnection) {
			conn = null;
			deleteTransaction();
		}

		if (!transactionMode) {
			String sMsg = "TENTATIVO DI CHIUDERE UNA TRANSAZIONE COME CONNESSIONE NORMALE";
			if (DaoBase.isGlobalPrintMode()) {
				log.debug(sMsg);
			}
		}

		if (objConn.isClosed())
			return;

		if (DaoBase.isGlobalPrintMode()) {
			log.debug("CLOSE TRANSACTION " + connectionName + " ROLLBACK");
		}

		objConn.rollback();

		if (closeConnection) {
			objConn.setAutoCommit(true);
			objConn.close();
		}

		objConn = null;

		transactionMode = true;

	}

	/**
	 * 
	 * @throws SQLException
	 * @throws DaoException
	 */
	public void commitTransaction() throws SQLException, DaoException {
		Log log = getLog();
		// prima di fare altro cancelliamo la connessione
		Connection objConn = this.conn;
		conn = null;
		
		deleteTransaction();

		if (!transactionMode) {
			String sMsg = "TENTATIVO DI CHIUDERE UNA TRANSAZIONE COME CONNESSIONE NORMALE";
			if (DaoBase.isGlobalPrintMode()) {
				log.debug(sMsg);
			}
			rollbackTransaction(); // rollback

			DaoException ex = new DaoException(sMsg);
			log.error(ex);
			throw ex;
		}

		// se siamo in testmode facciamo comunque un rollback
		if (isTestMode()) {
			objConn.rollback();
			if (DaoBase.isGlobalPrintMode()) {
				log.debug("CLOSE TRANSACTION " + connectionName + " COMMIT (TEST MODE)");
			}

		} else {
			objConn.commit();
			if (DaoBase.isGlobalPrintMode()) {
				log.debug("CLOSE TRANSACTION " + connectionName + " COMMIT");
			}

		}
		objConn.setAutoCommit(true);
		objConn.close();

	}

	/**
	 * 
	 * @throws SQLException
	 * @throws DaoException
	 */
	public void rollbackTransaction() throws SQLException, DaoException {
		Log log = getLog();
		// prima di fare altro cancelliamo la connessione
		Connection objConn = this.conn;
		
		conn = null;

		deleteTransaction();

		if (!transactionMode) {
			String sMsg = "TENTATIVO DI CHIUDERE UNA TRANSAZIONE COME CONNESSIONE NORMALE";
			if (DaoBase.isGlobalPrintMode()) {
				log.debug(sMsg);
			}
		}

		if (objConn.isClosed())
			return;

		if (DaoBase.isGlobalPrintMode()) {
			log.debug("CLOSE TRANSACTION " + connectionName + " ROLLBACK");
		}

		objConn.rollback();
		objConn.setAutoCommit(true);
		objConn.close();

		objConn = null;

		transactionMode = true;
	}

	/**
	 * Toglie da tutti i dao la transazione definita dal DaoSession
	 * 
	 * @throws DaoException
	 * 
	 */
	private void deleteTransaction() throws DaoException {
		Log log = getLog();
		for (DaoSessionWorker i : dao) {
			if (DaoBase.isGlobalPrintMode()) {
				log.debug("REMOVE DAO : " + i.getClass().toString());
			}
			i.setTransaction(null);
			i.setDaoSession(null);
		}
	}

	/**
	 * Recupera una connessione. Viene invocato il metodo getConnection dalla
	 * classe base DaoBase.
	 * 
	 * @return Connection
	 */
	private Connection getConnection() {
		Connection ret = null;
		Log log = getLog();

		try {
			ret = ConnectionPoolManager.getInstance().getConnection(connectionName);
		} catch (Exception e) {
			log.fatal("Impossibile recuperare connessione " + connectionName + ". Eccezione:" + e.toString());
		}

		return ret;
	}

}
